{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## ✅ Imports & Environment\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import asyncio\n",
        "import os\n",
        "from textwrap import dedent\n",
        "from typing import Dict, List, Tuple\n",
        "\n",
        "import gradio as gr\n",
        "from dotenv import load_dotenv\n",
        "\n",
        "from agents import Agent, Runner, function_tool, WebSearchTool, trace\n",
        "from agents.model_settings import ModelSettings\n",
        "from openai.types.responses import ResponseTextDeltaEvent\n",
        "from pydantic import BaseModel, Field\n",
        "from email.mime.text import MIMEText\n",
        "import smtplib\n",
        "\n",
        "load_dotenv(override=True)\n",
        "\n",
        "GMAIL_USER = os.getenv(\"GMAIL_USER\")\n",
        "GMAIL_APP_PASSWORD = os.getenv(\"GMAIL_APP_PASSWORD\")\n",
        "GMAIL_TO = os.getenv(\"GMAIL_TO\")\n",
        "DRY_RUN = os.getenv(\"DRY_RUN\", \"true\").lower() != \"false\"\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## ✅ Helper Utilities\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "def run_async(coro):\n",
        "    \"\"\"Synchronously execute an async coroutine (for notebook buttons).\"\"\"\n",
        "    return asyncio.run(coro)\n",
        "\n",
        "\n",
        "def parse_numbered_list(text: str) -> List[str]:\n",
        "    lines = [line.strip() for line in text.splitlines() if line.strip()]\n",
        "    questions = []\n",
        "    for line in lines:\n",
        "        if line[0].isdigit() and \".\" in line:\n",
        "            _, remainder = line.split(\".\", 1)\n",
        "            questions.append(remainder.strip())\n",
        "    return questions or lines\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## ✅ Agent Definitions\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "clarifier_agent = Agent(\n",
        "    name=\"Clarifying Question Generator\",\n",
        "    instructions=(\n",
        "        \"You create exactly three clarifying questions that will help tune research and personalization before \"\n",
        "        \"generating a cold email. Keep them concise.\"\n",
        "    ),\n",
        "    model=\"gpt-4o-mini\",\n",
        ")\n",
        "\n",
        "\n",
        "class SearchQueryPlan(BaseModel):\n",
        "    query: str = Field(description=\"A focused web search string to run in order to personalize the outreach.\")\n",
        "    reason: str = Field(description=\"Why this search will help tailor the message.\")\n",
        "\n",
        "\n",
        "class LeadResearchPlan(BaseModel):\n",
        "    insight_summary: str = Field(description=\"Three-sentence recap of the most relevant personalization angle.\")\n",
        "    search_queries: List[SearchQueryPlan] = Field(\n",
        "        description=\"Exactly three web searches to perform before writing outreach.\",\n",
        "        min_items=3,\n",
        "        max_items=3,\n",
        "    )\n",
        "\n",
        "\n",
        "lead_researcher = Agent(\n",
        "    name=\"Lead Research Strategist\",\n",
        "    instructions=(\n",
        "        \"Use the clarifications to create a structured research plan. Return exactly three search_queries along \"\n",
        "        \"with an insight_summary that ties everything together.\"\n",
        "    ),\n",
        "    model=\"gpt-4o-mini\",\n",
        "    output_type=LeadResearchPlan,\n",
        ")\n",
        "\n",
        "search_executor = Agent(\n",
        "    name=\"Web Search Executor\",\n",
        "    instructions=(\n",
        "        \"Run the provided search query using the WebSearchTool. The input will be in the format 'Query: ...' \"\n",
        "        \"followed by 'Reason: ...'. Incorporate the reason to focus your summary. Return bullet points (<=120 words).\"\n",
        "    ),\n",
        "    tools=[WebSearchTool(search_context_size=\"low\")],\n",
        "    model=\"gpt-4o-mini\",\n",
        "    model_settings=ModelSettings(tool_choice=\"required\"),\n",
        ")\n",
        "\n",
        "persona_instructions = {\n",
        "    \"Professional\": \"You write polished, data-backed cold emails for ComplAI, reinforcing audit readiness.\",\n",
        "    \"Engaging\": \"You write upbeat, story-driven cold emails for ComplAI, using light humor while staying credible.\",\n",
        "    \"Concise\": \"You write crisp cold emails for ComplAI, highlighting ROI in under 120 words.\",\n",
        "}\n",
        "\n",
        "sales_agents = {\n",
        "    persona: Agent(\n",
        "        name=f\"{persona} SDR\",\n",
        "        instructions=dedent(\n",
        "            f\"\"\"\n",
        "            You are a {persona.lower()} sales development rep for ComplAI.\n",
        "            {tone}\n",
        "            Use the clarifications and research digest provided. Output only the email body.\n",
        "            \"\"\"\n",
        "        ).strip(),\n",
        "        model=\"gpt-4o-mini\",\n",
        "    )\n",
        "    for persona, tone in persona_instructions.items()\n",
        "}\n",
        "\n",
        "reviewer_agent = Agent(\n",
        "    name=\"Email Reviewer\",\n",
        "    instructions=(\n",
        "        \"You compare multiple cold email drafts and return the single best one. Consider personalization, clarity, \"\n",
        "        \"and the strength of the call to action. Return only the winning email body.\"\n",
        "    ),\n",
        "    model=\"gpt-4o-mini\",\n",
        ")\n",
        "\n",
        "subject_writer = Agent(\n",
        "    name=\"Subject Line Writer\",\n",
        "    instructions=\"Write a compelling subject line for the provided cold email body.\",\n",
        "    model=\"gpt-4o-mini\",\n",
        ")\n",
        "\n",
        "html_converter = Agent(\n",
        "    name=\"HTML Formatter\",\n",
        "    instructions=\"Convert the cold email body to a clean HTML layout with short paragraphs and clear calls to action.\",\n",
        "    model=\"gpt-4o-mini\",\n",
        ")\n",
        "\n",
        "\n",
        "class EmailSendReport(BaseModel):\n",
        "    subject: str = Field(description=\"Subject line used for the email.\")\n",
        "    plain_text_body: str = Field(description=\"Plain text version of the winning email body.\")\n",
        "    html_body: str = Field(description=\"HTML formatted email body.\")\n",
        "    send_status: str = Field(description=\"Status returned from send_email (sent or dry_run).\")\n",
        "    detail: str | None = Field(default=None, description=\"Additional status details.\")\n",
        "\n",
        "\n",
        "@function_tool\n",
        "def send_email(subject: str, html_body: str) -> Dict[str, str]:\n",
        "    \"\"\"Send an HTML email via Gmail SMTP. Honours DRY_RUN to avoid accidental sends.\"\"\"\n",
        "    if DRY_RUN:\n",
        "        return {\"status\": \"dry_run\", \"detail\": \"DRY_RUN enabled; email not sent.\"}\n",
        "\n",
        "    if not all([GMAIL_USER, GMAIL_APP_PASSWORD, GMAIL_TO]):\n",
        "        raise RuntimeError(\"GMAIL_USER, GMAIL_APP_PASSWORD, and GMAIL_TO must be set or DRY_RUN must remain true.\")\n",
        "\n",
        "    msg = MIMEText(html_body, \"html\")\n",
        "    msg[\"Subject\"] = subject\n",
        "    msg[\"From\"] = GMAIL_USER\n",
        "    msg[\"To\"] = GMAIL_TO\n",
        "\n",
        "    with smtplib.SMTP(\"smtp.gmail.com\", 587) as server:\n",
        "        server.starttls()\n",
        "        server.login(GMAIL_USER, GMAIL_APP_PASSWORD)\n",
        "        server.sendmail(GMAIL_USER, [GMAIL_TO], msg.as_string())\n",
        "\n",
        "    return {\"status\": \"sent\", \"detail\": f\"Email sent to {GMAIL_TO}\"}\n",
        "\n",
        "\n",
        "def _email_manager_agent():\n",
        "    return Agent(\n",
        "        name=\"Email Manager\",\n",
        "        instructions=(\n",
        "            \"Receive the winning email body, generate a subject via the subject_writer tool, convert to HTML via \"\n",
        "            \"the html_converter tool, then call send_email with both. Respond strictly using the EmailSendReport schema.\"\n",
        "        ),\n",
        "        tools=[\n",
        "            subject_writer.as_tool(\"subject_writer\", \"Subject line generator\"),\n",
        "            html_converter.as_tool(\"html_converter\", \"Email HTML formatter\"),\n",
        "            send_email,\n",
        "        ],\n",
        "        model=\"gpt-4o-mini\",\n",
        "        output_type=EmailSendReport,\n",
        "    )\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## ✅ Core Async Workflow\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "async def generate_clarifying_questions(initial_request: str) -> List[str]:\n",
        "    result = await Runner.run(clarifier_agent, initial_request)\n",
        "    return parse_numbered_list(result.final_output)\n",
        "\n",
        "\n",
        "async def build_research_plan(initial_request: str, clarifications: Dict[str, str]) -> LeadResearchPlan:\n",
        "    clarifications_text = \"\\n\".join(f\"{k}: {v}\" for k, v in clarifications.items())\n",
        "    message = dedent(\n",
        "        f\"\"\"\n",
        "        Initial request: {initial_request}\n",
        "        Clarifications:\n",
        "        {clarifications_text}\n",
        "        \"\"\"\n",
        "    ).strip()\n",
        "    result = await Runner.run(lead_researcher, message)\n",
        "    return result.final_output\n",
        "\n",
        "\n",
        "async def execute_searches(plan: LeadResearchPlan) -> List[Dict[str, str]]:\n",
        "    summaries = []\n",
        "    for item in plan.search_queries:\n",
        "        prompt = f\"Query: {item.query}\\nReason: {item.reason}\"\n",
        "        result = await Runner.run(search_executor, prompt)\n",
        "        summaries.append({\n",
        "            \"query\": item.query,\n",
        "            \"reason\": item.reason,\n",
        "            \"findings\": result.final_output,\n",
        "        })\n",
        "    return summaries\n",
        "\n",
        "\n",
        "async def draft_email_variations(plan: LeadResearchPlan, search_summaries: List[Dict[str, str]], clarifications: Dict[str, str]) -> Dict[str, str]:\n",
        "    digest = dedent(\n",
        "        \"\\n\".join(\n",
        "            [plan.insight_summary, \"\\nResearch findings:\"] +\n",
        "            [f\"- {summary['findings']}\" for summary in search_summaries]\n",
        "        )\n",
        "    ).strip()\n",
        "\n",
        "    clar_text = \"\\n\".join(f\"{k}: {v}\" for k, v in clarifications.items())\n",
        "    message = dedent(\n",
        "        f\"\"\"\n",
        "        Clarifications:\n",
        "        {clar_text}\n",
        "\n",
        "        Research digest:\n",
        "        {digest}\n",
        "        \"\"\"\n",
        "    ).strip()\n",
        "\n",
        "    drafts = {}\n",
        "    for persona, agent in sales_agents.items():\n",
        "        result = await Runner.run(agent, message)\n",
        "        drafts[persona] = result.final_output\n",
        "    return drafts\n",
        "\n",
        "\n",
        "async def select_best_email(drafts: Dict[str, str]) -> str:\n",
        "    combined = \"\\n\\n\".join(f\"{persona} draft:\\n{body}\" for persona, body in drafts.items())\n",
        "    result = await Runner.run(reviewer_agent, combined)\n",
        "    return result.final_output\n",
        "\n",
        "\n",
        "async def format_and_send(winning_body: str, actually_send: bool) -> EmailSendReport:\n",
        "    global DRY_RUN\n",
        "    previous_state = DRY_RUN\n",
        "    DRY_RUN = not actually_send\n",
        "    try:\n",
        "        email_agent = _email_manager_agent()\n",
        "        result = await Runner.run(email_agent, winning_body)\n",
        "        return result.final_output\n",
        "    finally:\n",
        "        DRY_RUN = previous_state\n",
        "\n",
        "\n",
        "async def run_pipeline(initial_request: str, clarifications: Dict[str, str], send_email_flag: bool):\n",
        "    with trace(\"Research Outreach Pipeline\"):\n",
        "        plan = await build_research_plan(initial_request, clarifications)\n",
        "        searches = await execute_searches(plan)\n",
        "        drafts = await draft_email_variations(plan, searches, clarifications)\n",
        "        winning = await select_best_email(drafts)\n",
        "        email_result = await format_and_send(winning, send_email_flag)\n",
        "\n",
        "    detail = email_result.detail or \"\"\n",
        "    detail_note = f\" ({detail})\" if detail else \"\"\n",
        "    markdown_email = dedent(\n",
        "        f\"\"\"\n",
        "        ## Subject\n",
        "        {email_result.subject}\n",
        "\n",
        "        ## Email Body\n",
        "        {email_result.plain_text_body}\n",
        "\n",
        "        _Status: {email_result.send_status}{detail_note}_\n",
        "        \"\"\"\n",
        "    ).strip()\n",
        "\n",
        "    return {\n",
        "        \"markdown_email\": markdown_email,\n",
        "    }\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## ✅ Gradio Callbacks\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "def on_generate_questions(initial_request: str):\n",
        "    if not initial_request.strip():\n",
        "        raise gr.Error(\"Please enter a topic or brief first.\")\n",
        "    questions = run_async(generate_clarifying_questions(initial_request))\n",
        "    questions = questions[:3] + [\"\" for _ in range(max(0, 3 - len(questions)))]\n",
        "    return questions[:3]\n",
        "\n",
        "\n",
        "def on_run_pipeline(initial_request: str, answers: List[str], questions: List[str], send_email_flag: bool):\n",
        "    if not initial_request.strip():\n",
        "        raise gr.Error(\"Please provide the initial topic or brief.\")\n",
        "    if len(questions) < 3:\n",
        "        raise gr.Error(\"Generate clarifying questions first.\")\n",
        "\n",
        "    clarifications = {\n",
        "        questions[i]: answers[i] or \"Not provided\"\n",
        "        for i in range(3)\n",
        "    }\n",
        "\n",
        "    yield \"⏳ Running research and drafting email...\"\n",
        "\n",
        "    try:\n",
        "        results = run_async(run_pipeline(initial_request, clarifications, send_email_flag))\n",
        "    except Exception as exc:  # noqa: BLE001\n",
        "        raise gr.Error(f\"Pipeline failed: {exc}\") from exc\n",
        "\n",
        "    yield results[\"markdown_email\"]\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## ✅ Launch Gradio Interface\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "with gr.Blocks(title=\"Research-Guided Outreach\") as demo:\n",
        "    gr.Markdown(\n",
        "        \"\"\"\n",
        "        # Research-Guided Outreach Agent\n",
        "        Provide a topic or company initiative and let the agent clarify, research, draft, and (optionally) send an email.\n",
        "        \\n> **Note:** Web searches incur OpenAI's hosted search fee. Leave **Send email** unchecked to stay in dry-run mode.\n",
        "        \"\"\"\n",
        "    )\n",
        "\n",
        "    topic_input = gr.Textbox(label=\"Topic or outreach brief\", placeholder=\"e.g. Introduce ComplAI to Redwood Robotics\")\n",
        "    generate_btn = gr.Button(\"Generate clarifying questions\")\n",
        "    questions_state = gr.State([])\n",
        "\n",
        "    answer1 = gr.Textbox(label=\"Question 1\", placeholder=\"Answer to question 1\")\n",
        "    answer2 = gr.Textbox(label=\"Question 2\", placeholder=\"Answer to question 2\")\n",
        "    answer3 = gr.Textbox(label=\"Question 3\", placeholder=\"Answer to question 3\")\n",
        "\n",
        "    def _handle_generate(topic, current_questions):\n",
        "        questions = on_generate_questions(topic)\n",
        "        return (\n",
        "            gr.update(label=questions[0] or \"Question 1\"),\n",
        "            gr.update(label=questions[1] or \"Question 2\"),\n",
        "            gr.update(label=questions[2] or \"Question 3\"),\n",
        "            questions,\n",
        "        )\n",
        "\n",
        "    generate_btn.click(\n",
        "        fn=_handle_generate,\n",
        "        inputs=[topic_input, questions_state],\n",
        "        outputs=[answer1, answer2, answer3, questions_state],\n",
        "    )\n",
        "\n",
        "    send_toggle = gr.Checkbox(label=\"Send email via Gmail\", value=False)\n",
        "    run_btn = gr.Button(\"Run research and draft email\")\n",
        "\n",
        "    email_markdown = gr.Markdown(label=\"Email (Markdown)\")\n",
        "\n",
        "    def _handle_run(topic, a1, a2, a3, send_flag, stored_questions):\n",
        "        if not stored_questions:\n",
        "            raise gr.Error(\"Generate clarifying questions first.\")\n",
        "        for update in on_run_pipeline(topic, [a1, a2, a3], stored_questions, send_flag):\n",
        "            yield update\n",
        "\n",
        "    run_btn.click(\n",
        "        fn=_handle_run,\n",
        "        inputs=[topic_input, answer1, answer2, answer3, send_toggle, questions_state],\n",
        "        outputs=email_markdown,\n",
        "    )\n",
        "\n",
        "\n",
        "demo\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# Uncomment to launch the UI from the notebook\n",
        "demo.launch(inbrowser=True)\n"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": ".venv",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.12.12"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 2
}
